/*
**    Copyright (C) 2003-2010 Institute for Systems Biology
**                            Seattle, Washington, USA.
**
**    This library is free software; you can redistribute it and/or
**    modify it under the terms of the GNU Lesser General Public
**    License as published by the Free Software Foundation; either
**    version 2.1 of the License, or (at your option) any later version.
**
**    This library is distributed in the hope that it will be useful,
**    but WITHOUT ANY WARRANTY; without even the implied warranty of
**    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
**    Lesser General Public License for more details.
**
**    You should have received a copy of the GNU Lesser General Public
**    License along with this library; if not, write to the Free Software
**    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307  USA
*/

package org.systemsbiology.util;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;

/**
 * The complete representation of a recursive path processor, including treewalk and verification utilities
 * @author trobinso
 */
public class PathProcessor {

    private File outputDirectory = null;
    private long progressTicks = 0;

    private PathProcessor() {
        throw new UnsupportedOperationException("This utility library should not be instantiated directly.");
    }

    /**
     * Miniature exception specifying an error in processing
     */
    private abstract static class ProcessingException extends Exception {}

    /**
     * Miniature inner class that we use to inject a callback into our treewalk method
     */
    private abstract static class ProcessingCallback {
        /**
         * The actual callback code. Implementers are expected to throw their most relevant subset of Exception should
         *  any anomalies occur during processing, which callers are expected to catch if they don't wish to terminate
         *  abruptly.
         * @param f {@link java.io.File} specifying the given path
         * @throws IOException if the desired path suffers an IO-related problem
         * @throws ProcessingException if a callback-defined exception occurs during processing
         */
        public abstract void processPath(File f) throws IOException,ProcessingException;
    }

    /**
     * Recursive treewalk utility, using a specified callback processor to handle the contents of each file or directory
     *
     *  Each processor is expected to handle and throw its own Exceptions, which can and *will* be printed back to the
     *  user via {@code printHelp(e.getMessage())}. Please ensure your callbacks are tested for leaky Exceptions before
     *  attempting to use them.
     *
     *  Also note that this results in an implicit Exception squash to print information back to the user. To catch the
     *  Exceptions thrown here, drop a breakpoint in the catch blocks below.
     *
     * @param file {@link File} representing a branch of our recursion tree
     * @param callback {@link PathProcessor} supplied by the caller, indicating any secondary tasks to perform
     * @param depth int signaling the maximal depth of the recursion tree. Because Java auto-expands symlinks, a
     *               symlink loop is entirely possible. Sane values for "infinite" can be around or North of 65,535 for
     *               most systems.
     * @throws IOException if the desired path suffers an IO-related problem
     * @throws ProcessingException if a callback-defined exception occurs during processing
     */
    public synchronized void treewalk(File file, ProcessingCallback callback, int depth)
            throws IOException,ProcessingException {
        callback.processPath(file);// Process each input File f individually.
        //  Recursive treewalking is handled below.

        if (depth != 0 && file.isDirectory()) {
            assert(file.canExecute());
            assert(file.canRead());

            String[] contents;
            try {
                contents = file.list();
            } catch (SecurityException e) {
                throw e;                // The less friendly version of our permissions checks. If we hit this
                //  condition, we cheerfully stacktrace, as something clearly went wrong in our
                //  initial checks to ensure all targets were both readable and traversable.
            }
            for (String s: contents) {
                treewalk(new File(file, s), callback, depth-1);
            }
        }
    }

    /**
     * A small block of sanity checks we wish to keep separate from input path verification, for legibility purposes
     * @param filePaths {@link java.util.ArrayList}&lt{@link String}&gt; list of unique file paths to scan
     * @throws IOException if a sanity check fails
     */
    public void ensureOutputDirectory(ArrayList<String> filePaths) throws IOException {

        // Sanity checks on the output directory
        if (!outputDirectory.exists()) {
            try {
                boolean rst = outputDirectory.mkdir();
                assert(rst);
            } catch (SecurityException e) {

                throw new IOException("Target directory '" + outputDirectory + "' cannot be created." +
                        " Please check the enclosing directory permissions or specify a new location.");
            }
            assert(outputDirectory.exists());
        }

        // Note: java.io.File always expands symlinks if it is able to. As such, the file check should properly
        //  resolve these targets as directories instead of files.

        // "Any non-directory file created by a Java application is guaranteed to be a normal file."
        if (!outputDirectory.isDirectory()) {
            throw new IOException("Invalid directory path specified: '" + outputDirectory + "'. " +
                    "Target appears to be a file.\n" +
                    "Please check this path and verify that your file system is in a consistent state.");
        }

        // Ensure the target directory is traversable
        if (!outputDirectory.canExecute()) {
            throw new IOException("Target directory '" + outputDirectory + "' cannot be traversed." +
                    " Please update its permissions (executable) or specify a new location.");
        }

        // Ensure the target directory's list of filenames can be read
        if (!outputDirectory.canRead()) {
            throw new IOException("Target directory '" + outputDirectory + "' cannot be read." +
                    " Please update its permissions (read) or specify a new location.");
        }

        // Ensure the target directory can be written into
        if (!outputDirectory.canWrite()) {
            throw new IOException("Target directory '" + outputDirectory + "' cannot be written into." +
                    " Please update its permissions (write) or specify a new location.");
        }
    }

    /**
     * Verify all input paths are readable, no exceptions
     *
     *  Comically, we *do* handle all Exceptions, returning their root cause back to the callee.
     * @param filePaths {@link ArrayList}&lt{@link String}&gt; list of unique file paths to scan
     * @param recursive boolean indicating whether to use recursive operation
     * @throws IOException if the desired path suffers an IO-related problem
     * @throws ProcessingException if a callback-defined exception occurs during processing
     */
    public synchronized void verifyAllPaths(ArrayList<String> filePaths, boolean recursive)
            throws IOException,ProcessingException {
        // Ensure all input file paths exist and are readable. This includes an up-front, recursive check of
        //  readability, since we assume that the client will want to be informed of this problem before a potentially
        //  long parsing operation runs to completion.
        System.out.print("Scanning directories to ensure all paths are readable. This should only take a moment. ");
        for (String s: filePaths) {

            // Frontload File instantiation and test for existence, since further tests are nonsensical unless
            //  something *very bad* happens. In those cases, we'll complain during actual file parsing.
            File f = new File(s);
            if (!f.exists()) {
                throw new IOException("Input path '" + s + "' does not exist. Please verify the input path and try again.");
            }

            // Treewalk with our given validator
            treewalk(f, new ProcessingCallback(){
                public void processPath(File f) throws IOException,ProcessingException {
                    signalProgress(progressTicks, false);
                    assert(f != null);

                    if (!f.canRead()) {
                        // "Any non-directory file created by a Java application is guaranteed to be a normal file."
                        throw new IOException(
                                "Input " + (f.isDirectory() ? "directory '" : "file '") + f.getCanonicalPath() +
                                        "' cannot be read\n" +
                                        " Please update its permissions (read) and try again.");
                    }
                    if (f.isDirectory() && !f.canExecute()) {
                        throw new IOException(
                                "Input directory '" + f.getCanonicalPath() + "' cannot be traversed." +
                                        " Please update its permissions (execute) and try again.");
                    }
                    // No writable permissions should be required by reading of input files or directories
                }
            }, recursive ? 65535 : 0); // Set the maximum recursion depth to 65535 if we recurse, 0 otherwise.
        }
        System.out.println(" Done!");
    }

    /**
     * A utility function for displaying progress ticks against a caller-supplied long value
     *  Useful for showing the user that we're actually doing something during processing.
     * @param ticks long value given by the caller
     * @param includeLineBreak boolean true if a line break should be included after each ellipsis, false otherwise
     * @return ticks long updated ticks count
     */
    private static long signalProgress(long ticks, boolean includeLineBreak) {
        assert(ticks >= 0);

        if (ticks == Integer.MAX_VALUE) {
            ticks = 0;
        }

        // Prefer integer math over the use of the modulus operator, as we have a simplified special case
        if ((ticks / 500) * 500 == ticks) {
            if (includeLineBreak) {
                System.out.println("...");
            } else {
                System.out.print("...");
            }
        }
        ++ticks;
        return ticks;
    }
}
